﻿// (c)2010 David Schwarz (david.schwarz@live.com)
// This file is licensed under GNU GPL v2 license, see more at http://www.gnu.org/licenses/gpl-2.0.html
namespace Z2.Editor
{

	#region Using

	using System;
	using System.Collections.Generic;
	using System.Drawing;
	using System.Xml;
	using Z2.Common;
	using System.IO.Compression;
	using System.IO;
	using System.Diagnostics;
	using System.Text;
	using System.Windows.Forms;

	#endregion

	/// <summary>
	/// Třída reprezentující mapu hry
	/// </summary>
	public class World
	{

		#region Consts

		private const string XML_VERSION = "1.0";
		private const string XML_ENCODING = "UTF-8";

		#endregion

		#region Members

		private string m_script;
		private string m_author;
		private string m_name;
		private string m_fileName;
		private bool m_isNew;
		private Point m_start;
		private Dictionary<Point, EditorField> m_fields;
		private Dictionary<Point, EditorField> m_fieldsF;
		private List<EditorFieldObject> m_objects;
		private List<EditorTrigger> m_triggers;

		private string m_background;
		private Bitmap m_backgroundBitmap;

		#endregion

		#region Constructor

		/// <summary>
		/// Konstruktor pro načtení existující mapy
		/// </summary>
		/// <param name="fileName">Název souboru mapy</param>
		public World(string fileName)
			: this()
		{
			this.m_fileName = fileName;
			this.m_isNew = false;
		}

		/// <summary>
		/// Konstruktor pro vytvoření nové mapy
		/// </summary>
		public World()
		{
			this.m_fields = new Dictionary<Point, EditorField>();
			this.m_fieldsF = new Dictionary<Point, EditorField>();
			this.m_objects = new List<EditorFieldObject>();
			this.m_triggers = new List<EditorTrigger>();
			this.m_isNew = true;
		}

		#endregion

		#region Properties

		public Bitmap BackgroundBitmap
		{
			get { return this.m_backgroundBitmap; }
		}

		public string Background
		{
			get
			{
				return this.m_background;
			}
			set
			{
				this.m_background = value;
				this.m_backgroundBitmap = (Bitmap) Bitmap.FromFile(string.Format(@"{1}\Objects\{0}", value, Application.StartupPath));
			}
		}

		/// <summary>
		/// Vrací či nastavuje startovní pozici hráče
		/// </summary>
		public Point Start
		{
			get
			{
				return this.m_start;
			}
			set
			{
				this.m_start = value;
			}
		}

		/// <summary>
		/// Vrací <strong>True</strong>, pokud je mapa nová (nově vytvořená & neuložená)
		/// </summary>
		public bool IsNew
		{
			get
			{
				return this.m_isNew;
			}
		}

		/// <summary>
		/// Vrací název souboru, ve kterém jsou uloženy data mapy
		/// </summary>
		public string FileName
		{
			get
			{
				return this.m_fileName;
			}
		}

		public List<EditorFieldObject> Objects
		{
			get
			{
				return this.m_objects;
			}
		}

		/// <summary>
		/// Vrací seznam všech polí
		/// </summary>
		public Dictionary<Point, EditorField> Fields
		{
			get
			{
				return this.m_fields;
			}
		}

		public Dictionary<Point, EditorField> FrontFields
		{
			get
			{
				return this.m_fieldsF;
			}
		}

		/// <summary>
		/// Vrací či nastavuje název mapy
		/// </summary>
		public string Name
		{
			get
			{
				return this.m_name;
			}
			set
			{
				this.m_name = value;
			}
		}

		/// <summary>
		/// Vrací či nastavuje jméno autora
		/// </summary>
		public string Author
		{
			get
			{
				return this.m_author;
			}
			set
			{
				this.m_author = value;
			}
		}

		public string Script
		{
			get
			{
				return this.m_script?? string.Empty;
			}
			set
			{
				this.m_script = value;
			}
		}

		#endregion

		#region Methods
		
		/// <summary>
		/// Pomocná metoda pro vytoření Xml atributu
		/// </summary>
		/// <param name="name">Název atributu</param>
		/// <param name="value">Hodnota atributu</param>
		/// <param name="doc">Instance <see cref="XmlDocument"/></param>
		/// <returns><see cref="XmlAttribute"/></returns>
		protected static XmlAttribute CreateAttribute(string name, string value, XmlDocument doc)
		{
			XmlAttribute attribute = doc.CreateAttribute(name);
			attribute.Value = value;
			return attribute;
		}

		/// <summary>
		/// Uloží data levelu do souboru <see cref="FileName"/>
		/// </summary>
		public void Save()
		{
			this.Save(this.FileName);
		}

		/// <summary>
		/// Uloží data levelu do souboru <paramref name="fileName"/>
		/// </summary>
		/// <param name="fileName">Nový název souboru</param>
		public void Save(string fileName)
		{
			this.m_fileName = fileName;

			// Hranice mapy
			long boundX = 0;
			long boundY = 0;
			long boundW = 0;
			long boundH = 0;

			// Vytvoření XML dokumentu & hlavičky
			XmlDocument document = new XmlDocument();
			document.PreserveWhitespace = false;
			document.AppendChild(document.CreateXmlDeclaration(World.XML_VERSION, World.XML_ENCODING, string.Empty));
			
			// Vytvoříme si hlavní elementy mapy
			XmlElement map = document.CreateElement(XmlConst.ELM_MAP);
			XmlElement env = document.CreateElement(XmlConst.ELM_FIELDS);
			XmlElement obj = document.CreateElement(XmlConst.ELM_OBJECTS);
			XmlElement trg = document.CreateElement(XmlConst.ELM_TRIGGERS);
			XmlElement scr = document.CreateElement(XmlConst.ELM_SCRIPT);
			XmlElement bg = document.CreateElement(XmlConst.ELM_BACKGROUND);

			if (!string.IsNullOrEmpty(this.Background))
			{
				bg.Attributes.Append(CreateAttribute(XmlConst.ATT_NAME, this.Background, document));
				map.AppendChild(bg);
			}

			XmlElement elm = null;

			// Add all elements to the Map element
			map.AppendChild(env);
			map.AppendChild(obj);
			map.AppendChild(trg);
			map.AppendChild(scr);

			boundX = long.MaxValue;
			boundY = long.MaxValue;
			boundW = long.MinValue;
			boundH = long.MinValue;

			// Save script
			byte[] scriptData = Encoding.Unicode.GetBytes(this.Script);
			scr.InnerText = Convert.ToBase64String(scriptData);

			// Save all triggers
			foreach (EditorTrigger trigger in this.m_triggers)
			{
				// TODO
				//elm = document.CreateElement(XmlConst.ELM_TRIGGER);
				//trg.AppendChild(elm);
			}

			// Uložení všech políček a objektů
			// FRONT
			foreach (Point point in this.m_fieldsF.Keys)
			{
				EditorField field = null;
				// Výpočet hranic mapy
				boundX = Math.Min(boundX, point.X);
				boundY = Math.Min(boundY, point.Y);
				boundW = Math.Max(boundW, point.X);
				boundH = Math.Max(boundH, point.Y);

				field = this.m_fieldsF[point];

				if (field.EditorObject.Type != FieldType.Enviroment)
					throw new NotSupportedException();

				env.AppendChild(field.Save(XmlConst.ELM_FIELD, document));
			}
			// OTHERS
			foreach (Point point in this.m_fields.Keys)
			{
				EditorField field = null;
				// Výpočet hranic mapy
				boundX = Math.Min(boundX, point.X);
				boundY = Math.Min(boundY, point.Y);
				boundW = Math.Max(boundW, point.X);
				boundH = Math.Max(boundH, point.Y);

				field = this.m_fields[point];

				if(field.EditorObject.Type != FieldType.Enviroment)
					throw new NotSupportedException();

				env.AppendChild(field.Save(XmlConst.ELM_FIELD, document));
			}

			//// Uložení objektů
			foreach (EditorField field in this.m_objects)
			{
				if (field.EditorObject.Type != FieldType.Object)
					throw new NotSupportedException();

				obj.AppendChild(field.Save(XmlConst.ELM_OBJECT, document));
			}

			document.AppendChild(map);
			
			// Povinné rozšíření hranic
			++boundW;
			++boundH;

			// Uložení hodnot mapy
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_AUTHOR, this.Author, document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_NAME, this.Name, document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_STARTX, this.Start.X.ToString(), document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_STARTY, this.Start.Y.ToString(), document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_BOUNDX, boundX.ToString(), document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_BOUNDY, boundY.ToString(), document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_BOUNDW, boundW.ToString(), document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_BOUNDH, boundH.ToString(), document));
			
			using (Stream stream = File.Create(fileName))
			using (GZipStream gzip = new GZipStream(stream, CompressionMode.Compress))
			{
				document.Save(gzip);
			}
			
			this.m_isNew = false;
		}

		/// <summary>
		/// Metoda načte data mapy
		/// </summary>
		public void Load()
		{
			Debug.WriteLine(string.Format("Loading map {0}..", this.FileName));
			Stopwatch watch = new Stopwatch();
			watch.Start();

			EditorField.Clear();

			XmlDocument document = new XmlDocument();
			using(Stream stream = File.OpenRead(this.FileName))
			using (GZipStream gzip = new GZipStream(stream, CompressionMode.Decompress))
			{
				document.Load(gzip);
			}
			
			Debug.WriteLine("Unzipped.");

			XmlNodeList map = document.GetElementsByTagName(XmlConst.ELM_MAP);
			this.m_author = map[0].Attributes[XmlConst.ATT_AUTHOR].Value;
			this.m_name = map[0].Attributes[XmlConst.ATT_NAME].Value;
			this.m_start = new Point(
				Convert.ToInt32(map[0].Attributes[XmlConst.ATT_STARTX].Value),
				Convert.ToInt32(map[0].Attributes[XmlConst.ATT_STARTY].Value));

			// Získáme všechny elementy tiles a objektů
			XmlNodeList env = document.GetElementsByTagName(XmlConst.ELM_FIELD);
			XmlNodeList obj = document.GetElementsByTagName(XmlConst.ELM_OBJECT);
			XmlNodeList trg = document.GetElementsByTagName(XmlConst.ELM_TRIGGER);
			XmlNodeList scr = document.GetElementsByTagName(XmlConst.ELM_SCRIPT);
			XmlNodeList bg = document.GetElementsByTagName(XmlConst.ELM_BACKGROUND);

			//this.m_fieldsF.Clear();
			//this.m_fields.Clear();
			//this.m_objects.Clear();
			
			if (bg.Count > 0)
			{
				this.Background = bg[0].Attributes[XmlConst.ATT_NAME].Value;
			}

			if (scr.Count > 0)
			{
				byte[] scriptData = Convert.FromBase64String(scr[0].InnerText);
				this.Script = Encoding.Unicode.GetString(scriptData);
			}

			Debug.WriteLine(string.Format("Loading {0} fields.", env.Count));
			// Načteme env. fieldy
			foreach (XmlNode node in env)
			{
				EditorField field = new EditorField();
				field.Load(node);
				this.Put(field.Location.X, field.Location.Y, field); 
			}

			Debug.WriteLine(string.Format("Loading {0} objects.", obj.Count));

			// Objekty
			foreach (XmlNode node in obj)
			{
				EditorFieldObject field = new EditorFieldObject();
				field.Load(node);
				this.Put(field.Location.X, field.Location.Y, field); 
			}

			Debug.WriteLine(string.Format("Loading {0} triggers.", trg.Count));

			// Triggery
			foreach(XmlNode node in trg)
			{
				EditorTrigger trigger = new EditorTrigger();
				trigger.Load(node);
				this.Put(trigger.Location.X, trigger.Location.Y, trigger); 
			}

			watch.Stop();
			Debug.WriteLine(string.Format("Done. Load time: {0} ms.", watch.ElapsedMilliseconds));
		}

		/// <summary>
		/// Metoda smaže pole na pozici <paramref name="x"/>:<paramref name="y"/>
		/// </summary>
		/// <param name="x">X pozice</param>
		/// <param name="y">Y pozice</param>
		public void Delete(int x, int y)
		{
			Point point = new Point(x, y);
			if (this.m_fieldsF.ContainsKey(point))
			{
				this.m_fieldsF[point].Remove();
				this.m_fieldsF.Remove(point);
			}

			if (this.m_fields.ContainsKey(point))
			{
				this.m_fields[point].Remove();
				this.m_fields.Remove(point);
			}
		}

		/// <summary>
		/// Metoda smaže z mapy objekt
		/// </summary>
		/// <param name="obj">Objekt</param>
		public void DeleteObject(EditorFieldObject obj)
		{
			obj.Remove();
			this.m_objects.Remove(obj);
		}

		/// <summary>
		/// Metoda vloží do mapy objekt/tiles
		/// </summary>
		/// <param name="x">X pozice</param>
		/// <param name="y">Y pozice</param>
		/// <param name="field">Pole/Objekt</param>
		public void Put(int x, int y, EditorField field)
		{
			this.Put(x, y, field, field is EditorFieldObject);
		}

		/// <summary>
		/// Metoda vloží na pozici <paramref name="x"/>:<paramref name="y"/> tile
		/// </summary>
		/// <param name="x">X pozice</param>
		/// <param name="y">Y pozice</param>
		/// <param name="field"><see cref="EditorField"/></param>
		public void Put(int x, int y, EditorField field, bool isObject)
		{
			Point point = new Point(x, y);
			field.Location = point;
			if (isObject)
			{
				this.m_objects.Add((EditorFieldObject)field);
			}
			else
			{
				// decide where to store this one
				Dictionary<Point, EditorField> storage = 
					field.EditorObject.IsFront ? this.m_fieldsF : this.m_fields;
				
				if (storage.ContainsKey(point))
				{
					storage[point] = field;
					return;
				}

				storage.Add(point, field);
			}
		}

		/// <summary>
		/// Metoda vrací pole mapy na dané pozici. Pokud na dané pozici nic není, vrací <strong>null</strong>.
		/// </summary>
		/// <param name="x">X pozice</param>
		/// <param name="y">Y pozice</param>
		/// <returns>Pole mapy</returns>
		public EditorField Get(int x, int y)
		{
			return Get(x, y, false);
		}

		public EditorField Get(int x, int y, bool front)
		{
			Point point = new Point(x, y);
			if (front)
			{
				if (this.m_fieldsF.ContainsKey(point))
					return this.m_fieldsF[point];
			}
			else
			{
				if (this.m_fields.ContainsKey(point))
					return this.m_fields[point];
			}

			return null;
		}

		/// <summary>
		/// Metoda vrací objekt na dané pozici
		/// </summary>
		/// <param name="x">X pozice</param>
		/// <param name="y">Y pozice</param>
		/// <returns>Objekt mapy</returns>
		public List<EditorFieldObject> GetObject(int x, int y)
		{
			return m_objects.FindAll(
				match => 
					x >= (match.Location.X * 32) && x <= ((match.Location.X * 32) + match.EditorObject.FrameWidth) &&
					y >= (match.Location.Y * 32) && y <= ((match.Location.Y * 32) + match.EditorObject.FrameHeight));
		}

		#endregion

	}
}
